﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Web;
using YasharEl.Infrastructure.ApplicationModel.Bootstrap.Configuration;
using YasharEl.Infrastructure.ApplicationModel.Bootstrap.Extensibility;
using YasharEl.Infrastructure.Diagnostics.Logging;
using YasharEl.Infrastructure.ObjectFactory;
using YasharEl.Infrastructure.ServiceBus.Configuration;
using YasharEl.Infrastructure.ServiceBus.MessageMapper;
using YasharEl.Infrastructure.ServiceBus.MessageMapper.Reflection;
using YasharEl.Infrastructure.ServiceBus.Serialization;
using YasharEl.Infrastructure.ServiceBus.Serialization.Xml;
using YasharEl.Infrastructure.ServiceBus.Unicast;
using YasharEl.Infrastructure.ServiceBus.Unicast.Queuing;
using YasharEl.Infrastructure.ServiceBus.Unicast.Queuing.Msmq;
using YasharEl.Infrastructure.ServiceBus.Unicast.Subscriptions;
using YasharEl.Infrastructure.ServiceBus.Unicast.Subscriptions.Msmq;
using YasharEl.Infrastructure.ServiceBus.Unicast.Utils;
using YasharEl.Infrastructure.ServiceBus.Unicast.Transport;
using YasharEl.Infrastructure.ServiceBus.Unicast.Transport.Transactional;
using System.Transactions;
using YasharEl.Infrastructure.ServiceBus.Unicast.FaultTolerance.Forwarder;
using YasharEl.Infrastructure.ServiceBus.Unicast.FaultTolerance;
using YasharEl.Infrastructure.ApplicationModel.Modularity.Bootstrap;

namespace YasharEl.Infrastructure.ServiceBus.ApplicationModel.Bootstrap
{
    public class ServiceBusBootstrapperExtension : ModuleComponentBootstrappingExtension, IConsumeConfigurationSection
    {
        #region Protected Properties

        /// <summary>
        /// Key is unicast endpoint name.
        /// Value is a map of which message types (belonging to the given assemblies) are owned 
        /// by which endpoint.
        /// </summary>
        protected IDictionary<string, IDictionary<Type, Address>> BusesToEndpoints = new Dictionary<string, IDictionary<Type, Address>>();

        #endregion

        #region Properties

        public ServiceBusConfig ServiceBusConfigSection { get; private set; }

        #endregion

        #region IConsumeConfigurationSection Members

        public void Apply(ConfigurationSection section)
        {
            ServiceBusConfigSection = section as ServiceBusConfig;
        }

        #endregion

        #region BaseComplexBootstrappingExtension Overrides

        public override string Describe()
        {
            return "Extension which configure and start service bus component.";
        }

        public override void Configure()
        {
            base.Configure();

            if (ServiceBusConfigSection == null || ServiceBusConfigSection.Endpoints == null ||
                ServiceBusConfigSection.Endpoints.Count == 0)
            {
                Logger.Warn("There is no service bus endpoints to configure.");
                return;
            }

            foreach (EndpointConfig endpoint in ServiceBusConfigSection.Endpoints)
            {
                Logger.Info("Configuring service bus endpoint {0} ...", endpoint.Name);

                BaseDependencyContainer endpointContainer = 
                    (BaseDependencyContainer)Container.CreateChildContainer(endpoint.Name);

                BusesToEndpoints[endpoint.Name] = new Dictionary<Type, Address>();

                UnicastBus unicastBus = new UnicastBus(
                    Container.DependencyResolver.Resolve<ILogger<UnicastBus>>(),
                    endpointContainer.DependencyResolver
                );

                ConfigureMessageSerialization(endpoint, unicastBus);

                // Configure master node address
                ConfigureMasterNodeAddress(endpoint, unicastBus);

                // Configure timeout manager address,
                // Use config valie if defined, 
                // if not uses "EndpointName.Timeouts" naming convention.
                ConfigureTimeoutManagerAddress(endpoint, unicastBus);

                // Configure and register subscription authorization dependency
                ConfigureSubscriptionAuthorization(endpoint, unicastBus);

                // Configure and register message modules
                ConfigureMessageModules(endpoint, unicastBus);

                ScanAndRegisterLocalMessages(endpoint);

                ConfigureBusProperties(endpoint, unicastBus);

                ConfigureSubscriptionStorage(endpoint, unicastBus);

                ConfigureFaultManager(endpoint, unicastBus);

                ConfigureTransport(endpoint, unicastBus);

                ConfigureMessageHandlers(endpoint, unicastBus);

                endpointContainer.DependencyRegistrar.Register<IUnicastBus>(unicastBus, true);
                endpointContainer.DependencyRegistrar.Register<IStartableBus>(unicastBus, true);
                endpointContainer.DependencyRegistrar.Register<UnicastBus>(unicastBus, true);

                Container.DependencyRegistrar.Register<IUnicastBus>(unicastBus, endpoint.Name, true);
                Container.DependencyRegistrar.Register<IStartableBus>(unicastBus, endpoint.Name, true);
                Container.DependencyRegistrar.Register<UnicastBus>(unicastBus, endpoint.Name, true);

                Logger.Info("{0} service bus endpoint successfully configured.", endpoint.Name);
            }
        }

        public override void Start()
        {
            base.Start();

            if (BusesToEndpoints != null && BusesToEndpoints.Count > 0)
            {
                foreach (var busEndpointMapping in BusesToEndpoints)
                {
                    IStartableBus busStarter = 
                        Container.DependencyResolver.Resolve<IStartableBus>(busEndpointMapping.Key);

                    Logger.Debug("Starting service bus endpoint {0} ...", busEndpointMapping.Key);

                    IBus bus = busStarter.Start();
                }
            }
            else
            {
                Logger.Debug("No service bus endpoints to start...");
            }
        }

        public override void Shutdown()
        {
            base.Shutdown();

            if (BusesToEndpoints != null && BusesToEndpoints.Count > 0)
            {
                foreach (var busEndpointMapping in BusesToEndpoints)
                {
                    IStartableBus busStarter =
                        Container.DependencyResolver.Resolve<IStartableBus>(busEndpointMapping.Key);

                    Logger.Debug("Shutdown service bus endpoint {0} ...", busEndpointMapping.Key);

                    busStarter.Dispose();
                }
            }
            else
            {
                Logger.Debug("No service bus endpoints to shutdown...");
            }
        }

        #endregion

        #region Configuration Helper Methods

        private void ConfigureMessageSerialization(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            IMessageSerializer messageSerializer = null;
            IMessageMapper messageMapper = new ReflectionMessageMapper(
                Container.DependencyResolver.Resolve<ILogger<ReflectionMessageMapper>>()
            );
            IDependencyRegistrar registrar = unicastBus.DependencyResolver.Resolve<IDependencyRegistrar>();

            registrar.Register<IMessageMapper>(messageMapper, true);
            registrar.Register<IMessageCreator>(messageMapper, true);

            if (endpoint.UnicastBus.MessageSerializer != null)
            {
                XmlMessageSerializerConfig xmlConfig = endpoint.UnicastBus.MessageSerializer as XmlMessageSerializerConfig;
                if (xmlConfig != null)
                {
                    XmlMessageSerializer xml = new XmlMessageSerializer(
                        messageMapper, Container.DependencyResolver.Resolve<ILogger<XmlMessageSerializer>>()
                    );
                    if (xmlConfig.Namespace != null)
                    {
                        xml.Namespace = xmlConfig.Namespace;
                    }
                    messageSerializer = xml;
                    
                    registrar.Register<XmlMessageSerializer>(xml, true);

                    Logger.Debug("Xml message serializer configured...");
                }
                else
                {
                    BinaryMessageSerializerConfig binaryConfig = endpoint.UnicastBus.MessageSerializer as BinaryMessageSerializerConfig;
                    if (binaryConfig != null)
                    {
                        // TODO : Read and create binary serializer
                    }
                }
            }
            else
            {
                Logger.Warn("MessageSerializer not specified in service bus endpoint {0}. Xml message serializer used by default.", endpoint.Name);

                messageSerializer = new XmlMessageSerializer(
                    messageMapper, Container.DependencyResolver.Resolve<ILogger<XmlMessageSerializer>>()
                );
                
                registrar.Register<XmlMessageSerializer>((XmlMessageSerializer)messageSerializer, true);
            }

            registrar.Register<IMessageSerializer>(messageSerializer, true);

            unicastBus.MessageSerializer = messageSerializer;
            unicastBus.MessageMapper = messageMapper;
        }

        private void ConfigureMasterNodeAddress(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            if (!string.IsNullOrWhiteSpace(endpoint.UnicastBus.MasterNodeAddress))
            {
                ValidateHostName(endpoint.Name, endpoint.UnicastBus.MasterNodeAddress);

                unicastBus.MasterNodeAddress = new Address(endpoint.Name, endpoint.UnicastBus.MasterNodeAddress);
            }
            else
            {
                unicastBus.MasterNodeAddress = Address.Parse(endpoint.Name);
            }
        }

        /// <summary>
        /// Uses "TimeoutManagerAddress" parameter form config file if defined, 
        /// if not, uses "EndpointName.Timeouts".
        /// </summary>
        /// <param name="endpoint"></param>
        /// <param name="unicastBus"></param>
        private void ConfigureTimeoutManagerAddress(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            if (!string.IsNullOrWhiteSpace(endpoint.UnicastBus.TimeoutManagerAddress))
            {
                unicastBus.MasterNodeAddress = Address.Parse(endpoint.UnicastBus.TimeoutManagerAddress);
            }
            else
            {
                unicastBus.MasterNodeAddress = Address.Parse(endpoint.Name).SubScope("Timeouts");
            }
        }

        private void ConfigureSubscriptionAuthorization(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            if (endpoint.UnicastBus.SubscriptionAuthorizerType != null)
            {
                unicastBus.DependencyResolver
                    .Resolve<IDependencyRegistrar>()
                    .Register( // Register IAuthorizeSubscriptions implementor as singleton
                        typeof(IAuthorizeSubscriptions),
                        endpoint.UnicastBus.SubscriptionAuthorizerType,
                        true
                    );
            }
        }

        private void ConfigureMessageModules(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            if (endpoint.UnicastBus.MessageModules != null &&
                endpoint.UnicastBus.MessageModules.Count > 0)
            {
                IDependencyRegistrar dependencyRegistrar = unicastBus.DependencyResolver.Resolve<IDependencyRegistrar>();
                foreach (MessageModuleConfig messageModuleConfig in endpoint.UnicastBus.MessageModules)
                {
                    dependencyRegistrar.Register(
                        typeof(IMessageModule),
                        messageModuleConfig.MessageModuleType,
                        false);
                }
            }
        }

        private void ConfigureBusProperties(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            WindowsIdentity identity = WindowsIdentity.GetCurrent();
            unicastBus.InputAddress = Address.Parse(endpoint.Name);
            MsmqUtilities.CreateQueueIfNecessary(unicastBus.InputAddress, identity.Name);

            if (!string.IsNullOrWhiteSpace(endpoint.UnicastBus.ForwardReceivedMessagesTo))
            {
                unicastBus.ForwardReceivedMessagesTo = Address.Parse(endpoint.UnicastBus.ForwardReceivedMessagesTo);
                MsmqUtilities.CreateQueueIfNecessary(unicastBus.ForwardReceivedMessagesTo, identity.Name);
            }
            else
            {
                unicastBus.ForwardReceivedMessagesTo = Address.Undefined;
            }

            if (endpoint.UnicastBus.TimeToBeReceivedOnForwardedMessages != null)
            {
                unicastBus.TimeToBeReceivedOnForwardedMessages = endpoint.UnicastBus.TimeToBeReceivedOnForwardedMessages;
            }

            if (endpoint.UnicastBus.MessageEndpointMappings != null &&
                endpoint.UnicastBus.MessageEndpointMappings.Count > 0)
            {
                foreach (MessageEndpointMappingConfig mapping in endpoint.UnicastBus.MessageEndpointMappings)
                {
                    try
                    {
                        var messageType = Type.GetType(mapping.Messages, false);
                        if (messageType != null)
                        {
                            BusesToEndpoints[endpoint.Name][messageType] = Address.Parse(mapping.Endpoint);
                            continue;
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Error(ex, "Problem loading message type: " + mapping.Messages);
                        throw;
                    }

                    try
                    {
                        var a = Assembly.Load(mapping.Messages);
                        foreach (var t in a.GetTypes())
                        {
                            BusesToEndpoints[endpoint.Name][t] = Address.Parse(mapping.Endpoint);
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Error(ex, "Problem loading message assembly: " + mapping.Messages);
                        throw new InvalidServiceBusConfigurationException(
                            "Problem loading message assembly: " + mapping.Messages, ex);
                    }
                }
            }

            unicastBus.AutoSubscribe = endpoint.UnicastBus.AutoSubscribe;

            unicastBus.MessageOwners = BusesToEndpoints[endpoint.Name];
        }

        private void ConfigureSubscriptionStorage(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            if (endpoint.UnicastBus.SubscriptionStorage != null)
            {
                IDependencyRegistrar registrar = unicastBus.DependencyResolver.Resolve<IDependencyRegistrar>();

                MsmqSubscriptionStorageConfig msmqConfig = endpoint.UnicastBus.SubscriptionStorage as MsmqSubscriptionStorageConfig;
                if (msmqConfig != null)
                {
                    MsmqSubscriptionStorage msmq = new MsmqSubscriptionStorage(
                        unicastBus.DependencyResolver.Resolve<ILogger<ISubscriptionStorage>>()
                    );
                    msmq.Queue = Address.Parse(msmqConfig.Queue);
                    msmq.DontUseExternalTransaction = msmqConfig.DontUseExternalTransaction;

                    WindowsIdentity identity = WindowsIdentity.GetCurrent();
                    MsmqUtilities.CreateQueueIfNecessary(msmq.Queue, identity.Name);

                    registrar.Register<ISubscriptionStorage>(msmq, true);
                    registrar.Register<MsmqSubscriptionStorage>(msmq, true);
                    unicastBus.SubscriptionStorage = msmq;

                    Logger.Debug("Msmq subsription storage configured...");
                }
            }
        }

        private void ConfigureFaultManager(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            if( endpoint.UnicastBus.FaultManager != null )
            {
                IDependencyRegistrar registrar = unicastBus.DependencyResolver.Resolve<IDependencyRegistrar>();

                FaultForwarderManagerConfig faultForwarderConfig = endpoint.UnicastBus.FaultManager as FaultForwarderManagerConfig;
                if (faultForwarderConfig != null)
                {
                    FaultForwarder faultForwarder = new FaultForwarder(
                        unicastBus.DependencyResolver.Resolve<ILogger<FaultForwarder>>(),
                        unicastBus.DependencyResolver
                    );
                    faultForwarder.ErrorQueue = Address.Parse(faultForwarderConfig.ErrorQueue);

                    WindowsIdentity identity = WindowsIdentity.GetCurrent();
                    MsmqUtilities.CreateQueueIfNecessary(faultForwarder.ErrorQueue, identity.Name);

                    registrar.Register<IManageMessageFailures>(faultForwarder, true);
                    registrar.Register<FaultForwarder>(faultForwarder, true);

                    unicastBus.FailureManager = faultForwarder;
                }

                Logger.Debug("Faults messages manager configured...");
            }
        }

        private void ConfigureTransport(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            IDependencyRegistrar registrar = unicastBus.DependencyResolver.Resolve<IDependencyRegistrar>();
            ISendMessages sender = null;
            IReceiveMessages receiver = null;
            ITransport transport = null;
            MsmqTransportConfig msmqTransportConfig = endpoint.UnicastBus.Transport as MsmqTransportConfig;
            if (msmqTransportConfig != null)
            {
                MsmqMessageSender msmqSender = new MsmqMessageSender();
                msmqSender.UseJournalQueue = msmqTransportConfig.UseJournalQueue;
                msmqSender.UseDeadLetterQueue = msmqTransportConfig.UseDeadLetterQueue;
                sender = msmqSender;
                registrar.Register<MsmqMessageSender>(msmqSender, true);

                MsmqMessageReceiver msmqReceiver = new MsmqMessageReceiver(
                    Container.DependencyResolver.Resolve<ILogger<MsmqMessageReceiver>>()
                );
                msmqReceiver.PurgeOnStartup = msmqTransportConfig.PurgeOnStartup;
                msmqReceiver.SecondsToWaitForMessage = msmqTransportConfig.SecondsToWaitForMessage <= 0 ? 1 : msmqTransportConfig.SecondsToWaitForMessage;
                receiver = msmqReceiver;
                registrar.Register<MsmqMessageReceiver>(msmqReceiver, true);

                TransactionalTransport transactionalTransport = new TransactionalTransport(
                    Container.DependencyResolver.Resolve<ILogger<TransactionalTransport>>()
                );
                transactionalTransport.IsTransactional = msmqTransportConfig.IsTransactional;
                if( transactionalTransport.IsTransactional )
                {
                    transactionalTransport.IsolationLevel = msmqTransportConfig.IsolationLevel;
                    transactionalTransport.TransactionTimeout = msmqTransportConfig.TransactionTimeout;
                }

                transactionalTransport.SupressDTC = msmqTransportConfig.SupressDTC;
                transactionalTransport.NumberOfWorkerThreads = msmqTransportConfig.NumberOfWorkerThreads;
                transactionalTransport.MaxRetries = msmqTransportConfig.MaxRetries;
                transactionalTransport.MessageReceiver = msmqReceiver;

                transactionalTransport.FailureManager = unicastBus.FailureManager;

                transport = transactionalTransport;
                registrar.Register<TransactionalTransport>(transactionalTransport, true);
            }
            else
            {
            }

            if (sender != null)
            {
                registrar.Register<ISendMessages>(sender, true);
                unicastBus.MessageSender = sender;
            }

            if (receiver != null)
            {
                registrar.Register<IReceiveMessages>(receiver, true);
            }

            if (transport != null)
            {
                registrar.Register<ITransport>(transport, true);
                unicastBus.Transport = transport;
            }
        }

        private void ScanAndRegisterLocalMessages(EndpointConfig endpoint)
        {
            ScannedTypes
                .Where(t => t.IsMessageType())
                .ToList()
                .ForEach(t => BusesToEndpoints[endpoint.Name][t] = Address.Undefined);
        }

        /// <summary>
        /// Loads all message handler assemblies in the runtime directory.
        /// </summary>
        /// <returns></returns>
        private void ConfigureMessageHandlers(EndpointConfig endpoint, UnicastBus unicastBus)
        {
            var types = new List<Type>();

            ScannedTypes
                .Where(TypeSpecifiesMessageHandlerOrdering)
                .ToList()
                .ForEach(t =>
                {
                    Logger.Debug("Going to ask for message handler ordering from {0}.", t);

                    var order = new Order();
                    ((ISpecifyMessageHandlerOrdering)Activator.CreateInstance(t)).SpecifyOrder(order);

                    order.Types.ToList().ForEach(ht =>
                    {
                        if (types.Contains(ht))
                            throw new InvalidServiceBusConfigurationException(string.Format("The order in which the type {0} should be invoked was already specified by a previous implementor of ISpecifyMessageHandlerOrdering. Check the debug logs to see which other specifiers have been invoked.", ht));
                    });

                    types.AddRange(order.Types);
                });

            LoadMessageHandlers(types, endpoint, unicastBus);
        }

        /// <summary>
        /// Loads all message handler assemblies in the runtime directory
        /// and specifies that the handlers in the given 'order' are to 
        /// run before all others and in the order specified.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="order"></param>
        /// <returns></returns>
        private void LoadMessageHandlers<T>(First<T> order, EndpointConfig endpoint, UnicastBus unicastBus)
        {
            LoadMessageHandlers(order.Types , endpoint, unicastBus);
        }

        private void LoadMessageHandlers(IEnumerable<Type> orderedTypes, EndpointConfig endpoint, UnicastBus unicastBus)
        {
            //LoadMessageHandlersCalled = true;
            var types = new List<Type>(ScannedTypes);

            foreach (Type t in orderedTypes)
                types.Remove(t);

            types.InsertRange(0, orderedTypes);

            ConfigureMessageHandlersIn(types, endpoint , unicastBus);
        }

        /// <summary>
        /// Scans the given types for types that are message handlers
        /// then uses the Configurer to configure them into the container as single call components,
        /// finally passing them to the bus as its MessageHandlerTypes.
        /// </summary>
        /// <param name="types"></param>
        /// <returns></returns>
        private void ConfigureMessageHandlersIn(IEnumerable<Type> types, EndpointConfig endpoint, UnicastBus unicastBus)
        {
            var handlers = new List<Type>();

            IDependencyRegistrar dependencyRegistrar = unicastBus.DependencyResolver.Resolve<IDependencyRegistrar>();

            foreach (Type t in types.Where(IsMessageHandler))
            {
                dependencyRegistrar.Register(t, false);
                handlers.Add(t);
            }

            unicastBus.MessageHandlerTypes = handlers;

            var availableDispatcherFactories = ScannedTypes
              .Where(
                  factory =>
                  !factory.IsInterface && typeof(IMessageDispatcherFactory).IsAssignableFrom(factory))
              .ToList();

            var dispatcherMappings = GetDispatcherFactories(handlers, availableDispatcherFactories);

            unicastBus.MessageDispatcherMappings = dispatcherMappings;

            //configure the message dispatcher for each handler
            availableDispatcherFactories
                .ToList()
                .ForEach
                (
                    factory => dependencyRegistrar.Register(
                        typeof(IMessageDispatcherFactory), 
                        factory,
                        true)
                );
        }

        private IDictionary<Type, Type> GetDispatcherFactories(IEnumerable<Type> handlers, IEnumerable<Type> messageDispatcherFactories)
        {
            var result = new Dictionary<Type, Type>();

            var customFactories = messageDispatcherFactories
                .Where(t => t != typeof(DefaultDispatcherFactory))
                .Select(t => (IMessageDispatcherFactory)Activator.CreateInstance(t)).ToList();


            foreach (var handler in handlers)
            {
                var factory = customFactories.FirstOrDefault(f => f.CanDispatch(handler));

                var factoryTypeToUse = typeof(DefaultDispatcherFactory);

                if (factory != null)
                    factoryTypeToUse = factory.GetType();

                result.Add(handler, factoryTypeToUse);
            }
            return result;
        }

        /// <summary>
        /// Returns true if the given type is a message handler.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private bool IsMessageHandler(Type t)
        {
            if (t.IsAbstract)
                return false;

            return t.GetInterfaces().Select(GetMessageTypeFromMessageHandler).Any(messageType => messageType != null);
        }

        /// <summary>
        /// Returns the message type handled by the given message handler type.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private Type GetMessageTypeFromMessageHandler(Type t)
        {
            if (t.IsGenericType)
            {
                Type[] args = t.GetGenericArguments();
                if (args.Length != 1)
                    return null;

                Type handlerType = typeof(IMessageHandler<>).MakeGenericType(args[0]);
                if (handlerType.IsAssignableFrom(t))
                    return args[0];
            }

            return null;
        }

        private bool TypeSpecifiesMessageHandlerOrdering(Type t)
        {
            return typeof(ISpecifyMessageHandlerOrdering).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface;
        }

        #endregion

        #region Validation Helper Methods

        private void ValidateHostName(string endpointName, string hostName)
        {
            if (Uri.CheckHostName(hostName) == UriHostNameType.Unknown)
            {
                throw new InvalidServiceBusConfigurationException(
                    string.Format("The 'MasterNodeAddress' attribute of endpoint '{0}' configuration: '{1}' is not a valid DNS name.", endpointName, hostName));
            }
        }

        #endregion

        #region ModuleComponentBootstrappingExtension Overrides

        public override string SectionName
        {
            get { return ServiceBusConfig.ServiceBusConfigDefaultSectionName; }
        }

        #endregion
    }
}
